feat(bmi270): Implement FOC and CRT calibration features for bmi270#595
feat(bmi270): Implement FOC and CRT calibration features for bmi270#595finger563 merged 5 commits intoesp-cpp:mainfrom
Conversation
|
✅Static analysis result - no issues found! ✅ |
7ccbcf2 to
f4e0119
Compare
| // 1. The device must be COMPLETELY STATIONARY on a flat surface. | ||
| // 2. Enable these lines only when you need calibration (e.g., once after assembly). | ||
| // --------------------------------------------------------------------------- | ||
| #if 0 |
There was a problem hiding this comment.
Can you please put this behind a KConfig variable for the example that defaults to true? that way the automated builds in CI catch any potential breaking API changes and the example naturally tests the API.
| // print kalman filter outputs | ||
| text += fmt::format("{:03.3f},{:03.3f},{:03.3f},", (float)orientation.x, (float)orientation.y, | ||
| (float)orientation.z); | ||
| text += fmt::format("{:03.3f},{:03.3f},{:03.3f},", (float)gravity_vector.x, | ||
| (float)gravity_vector.y, (float)gravity_vector.z); | ||
|
|
||
| auto madgwick_orientation = madgwick_filter_fn(dt, accel, gyro); | ||
| float roll = madgwick_orientation.roll; | ||
| float pitch = madgwick_orientation.pitch; | ||
| float yaw = madgwick_orientation.yaw; | ||
| float vx = sin(pitch); | ||
| float vy = -cos(pitch) * sin(roll); | ||
| float vz = -cos(pitch) * cos(roll); | ||
|
|
||
| // print madgwick filter outputs | ||
| text += fmt::format("{:03.3f},{:03.3f},{:03.3f},", roll, pitch, yaw); | ||
| text += fmt::format("{:03.3f},{:03.3f},{:03.3f}", vx, vy, vz); | ||
|
|
There was a problem hiding this comment.
Is there a reason you removed these prints / calculations?
There was a problem hiding this comment.
Pull request overview
Adds calibration capabilities to the espp::Bmi270 driver to improve sensor accuracy by leveraging BMI270’s on-chip routines (FOC for accel/gyro offsets and CRT for gyro gain trim), along with example/docs updates to demonstrate usage.
Changes:
- Added new public driver APIs for accelerometer/gyroscope FOC, CRT, gyro self-test, and offset/gain enable + offset read/write helpers.
- Extended BMI270 detail types (FOC target struct, CRT/status enums) and added
fmtformatting for CRT status. - Updated the example and documentation to show optional calibration usage and expand “Advanced Features” docs.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 12 comments.
| File | Description |
|---|---|
| components/bmi270/include/bmi270_detail.hpp | Adds new calibration-related structs/enums and fmt formatter for CRT trigger status. |
| components/bmi270/include/bmi270.hpp | Implements FOC/CRT/self-test and exposes offset/gain control APIs; adjusts register map for new features. |
| components/bmi270/example/main/bmi270_example.cpp | Adds optional (disabled) calibration demo block; modifies CSV output behavior. |
| components/bmi270/example/README.md | Documents calibration features and adds troubleshooting/advanced-feature guidance. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
components/bmi270/include/bmi270.hpp
Outdated
| // Invert | ||
| int8_t off_x = static_cast<int8_t>(-(delta_x / 64)); | ||
| int8_t off_y = static_cast<int8_t>(-(delta_y / 64)); | ||
| int8_t off_z = static_cast<int8_t>(-(delta_z / 64)); | ||
|
|
||
| // Write offset registers | ||
| uint8_t data[3] = {static_cast<uint8_t>(off_x), static_cast<uint8_t>(off_y), static_cast<uint8_t>(off_z)}; | ||
| write_many_to_register(static_cast<uint8_t>(Register::ACC_OFF_COMP_0), data, 3, ec); |
There was a problem hiding this comment.
off_x/off_y/off_z are computed as int32/64 then cast directly to int8_t. For realistic deltas (e.g., ~±16384 at 2g), this can exceed the int8_t range and wrap, producing incorrect offsets. Clamp the computed value to [-128, 127] before casting/writing.
| // Prepare for FOC | ||
| if (saved_aps) { | ||
| clear_bits_in_register(static_cast<uint8_t>(Register::PWR_CONF), 0x01, ec); | ||
| if (ec) return false; | ||
| } | ||
| if (!saved_acc_en) { | ||
| set_bits_in_register(static_cast<uint8_t>(Register::PWR_CTRL), ACC_ENABLE, ec); | ||
| if (ec) return false; | ||
| std::this_thread::sleep_for(std::chrono::milliseconds(50)); | ||
| } |
There was a problem hiding this comment.
This calibration routine mutates device state (APS bit, ACC enable, ACC_CONF/RANGE) and has multiple early return false paths. On any failure after state changes, the original settings may not be restored, leaving the device in an unexpected mode. Consider using a scope-guard/RAII cleanup so APS, sensor enables, and config are restored on all exit paths (success or error).
components/bmi270/include/bmi270.hpp
Outdated
| if (!saved_gyr_en) enable_gyroscope(false, ec); | ||
| if (saved_aps) set_bits_in_register(static_cast<uint8_t>(Register::PWR_CONF), 0x01, ec); |
There was a problem hiding this comment.
In perform_gyro_foc, the restoration calls (enable_gyroscope(false, ec) and re-enabling APS) are not followed by if (ec) return false;. As written, the function can log success and return true even if restoration fails and ec is set.
| if (!saved_gyr_en) enable_gyroscope(false, ec); | |
| if (saved_aps) set_bits_in_register(static_cast<uint8_t>(Register::PWR_CONF), 0x01, ec); | |
| if (!saved_gyr_en) { | |
| enable_gyroscope(false, ec); | |
| if (ec) return false; | |
| } | |
| if (saved_aps) { | |
| set_bits_in_register(static_cast<uint8_t>(Register::PWR_CONF), 0x01, ec); | |
| if (ec) return false; | |
| } |
components/bmi270/include/bmi270.hpp
Outdated
| uint8_t status = (status_reg & 0x38) >> 3; | ||
|
|
||
| if (status != 0x00) { | ||
| logger_.error("CRT: Failed"); |
There was a problem hiding this comment.
When CRT fails, the code logs only "CRT: Failed" without the decoded status bits (already extracted into status). Including the status value (and/or mapping it to GTriggerStatus) would make failures diagnosable.
| logger_.error("CRT: Failed"); | |
| logger_.error("CRT: Failed, status=0x{:02X}", status); |
| // print time and raw IMU data | ||
| std::string text = ""; | ||
| text += fmt::format("{:.3f},", now / 1'000'000.0f); | ||
| text += | ||
| fmt::format("{:02.3f},{:02.3f},{:02.3f},", (float)accel.x, (float)accel.y, (float)accel.z); | ||
| text += fmt::format("{:03.3f},{:03.3f},{:03.3f},", (float)gyro.x, (float)gyro.y, (float)gyro.z); | ||
| text += fmt::format("{:02.1f},", temp); | ||
| // print kalman filter outputs | ||
| text += fmt::format("{:03.3f},{:03.3f},{:03.3f},", (float)orientation.x, (float)orientation.y, | ||
| (float)orientation.z); | ||
| text += fmt::format("{:03.3f},{:03.3f},{:03.3f},", (float)gravity_vector.x, | ||
| (float)gravity_vector.y, (float)gravity_vector.z); | ||
|
|
||
| auto madgwick_orientation = madgwick_filter_fn(dt, accel, gyro); | ||
| float roll = madgwick_orientation.roll; | ||
| float pitch = madgwick_orientation.pitch; | ||
| float yaw = madgwick_orientation.yaw; | ||
| float vx = sin(pitch); | ||
| float vy = -cos(pitch) * sin(roll); | ||
| float vz = -cos(pitch) * cos(roll); | ||
|
|
||
| // print madgwick filter outputs | ||
| text += fmt::format("{:03.3f},{:03.3f},{:03.3f},", roll, pitch, yaw); | ||
| text += fmt::format("{:03.3f},{:03.3f},{:03.3f}", vx, vy, vz); | ||
|
|
||
| fmt::print("{}\n", text); | ||
|
|
There was a problem hiding this comment.
The CSV data line now only prints time + accel + gyro + temp, but the header printed later still lists Kalman/Madgwick orientation and gravity columns. This makes the output CSV invalid/misleading. Either restore printing of those values in the data rows or update the header (and remove now-unused orientation/gravity locals).
components/bmi270/example/README.md
Outdated
| - Reduce the burst write size in the configuration to limit stack usage. | ||
| ```cpp | ||
| espp::Bmi270::Config config; | ||
| config.burst_write_size = 128; // Use 128-byte chunks (default is 0/unlimited) |
There was a problem hiding this comment.
The note says burst_write_size default is "0/unlimited", but in the driver 0 maps to a single chunk of config_file_size bytes (currently 8192). Consider updating this wording to avoid implying an unbounded write size.
| config.burst_write_size = 128; // Use 128-byte chunks (default is 0/unlimited) | |
| config.burst_write_size = 128; // Use 128-byte chunks (default 0 = one chunk of config_file_size bytes, e.g. 8192) |
components/bmi270/include/bmi270.hpp
Outdated
|
|
||
| /// Perform Component Re-Trim (CRT) | ||
| /// @param ec The error code to set if an error occurs | ||
| /// @return True if successful |
There was a problem hiding this comment.
Might be worth adding a note to the API docs that this function may wait / block for over 2.5 seconds - and however long the calibration file takes to write as well
components/bmi270/include/bmi270.hpp
Outdated
|
|
||
| /// Perform gyroscope Fast Offset Compensation (FOC) | ||
| /// @param ec The error code to set if an error occurs | ||
| /// @return True if successful |
There was a problem hiding this comment.
Might be worth adding a note to the API docs that this function will take / block for >5.5 seconds
components/bmi270/include/bmi270.hpp
Outdated
| /// Perform accelerometer Fast Offset Compensation (FOC) | ||
| /// @param accel_g_value Target values for FOC | ||
| /// @param ec The error code to set if an error occurs | ||
| /// @return True if successful |
There was a problem hiding this comment.
Might be worth adding a note to the API docs for this function that it will take / block for >= 2.5 seconds
components/bmi270/include/bmi270.hpp
Outdated
| return status == 0x00; | ||
| } | ||
|
|
||
| /// Abort CRT or Gyro Self Test |
components/bmi270/include/bmi270.hpp
Outdated
| return !ec; | ||
| } | ||
|
|
||
| /// Perform Gyro Self Test |
There was a problem hiding this comment.
And please mention that this function will block for >= 2 seconds
finger563
left a comment
There was a problem hiding this comment.
Thanks for the PR! Overall this code looks really good, just have a few suggestions for the example and for API docs
|
Hi @finger563, sorry for the delay! I've addressed all your feedback, including RAII (ScopeGuard), input clamping, and documentation updates. Also, I restored the example code—removing it was a mistake due to a local build error, but it's fixed now. Ready for review again. Thanks! |
finger563
left a comment
There was a problem hiding this comment.
Thanks for addressing the comments, this looks great! There are a couple documentation updates but once those are done I'll merge this and cut a release!
| } | ||
|
|
||
| /// Abort CRT or Gyro Self Test | ||
| bool abort_crt_gyro_self_test(std::error_code &ec) { |
There was a problem hiding this comment.
Please add API docs to this function
components/bmi270/include/bmi270.hpp
Outdated
| bool set_gyro_offset(int16_t x, int16_t y, int16_t z, std::error_code &ec) { | ||
| std::lock_guard<std::recursive_mutex> lock(base_mutex_); | ||
|
|
||
| // Check for valid 10-bit range [-512, 511] |
There was a problem hiding this comment.
Can you please indicate in the API docs for this function the valid range?
Description
Added Fast Offset Compensation (FOC) and Component Re-Trim (CRT) calibration
features to the BMI270 inertial measurement unit driver. These features provide
hardware-level offset and sensitivity calibration for both accelerometer and
gyroscope sensors.
Motivation and Context
BMI270 sensors experience manufacturing tolerance variations that affect
measurement accuracy. The built-in calibration features compensate for:
But espp/bmi270 doesn't support these feature.
Tested and verified against Bosch Sensortec's official BMI270 Sensor API library
implementation.
How has this been tested?
Using my esp32c3 based imu board, change example codes with calibration example.
Screenshots (if appropriate, e.g. schematic, board, console logs, lab pictures):
[BMI270 Example/I][0.306]: Starting example!
[BMI270 Example/I][0.316]: Found BMI270 at address: 0x68
[Bmi270/I][0.316]: Setting low power mode to disabled
[Bmi270/I][0.326]: Loading BMI270 configuration file (8192 B), this may take some time...
[Bmi270/I][0.546]: Configuration file loaded successfully
[Bmi270/I][0.546]: BMI270 initialized successfully
[BMI270 Example/I][0.546]: Performing Accelerometer FOC...
[Bmi270/I][4.396]: Accel FOC completed. Offsets: X=-10, Y=9, Z=2
[BMI270 Example/I][4.396]: Accelerometer FOC completed successfully
[BMI270 Example/I][4.396]: Performing Gyroscope FOC...
[Bmi270/I][10.806]: Gyro FOC completed. Offsets: X=-2, Y=-1, Z=0
[BMI270 Example/I][10.806]: Gyroscope FOC completed successfully
[BMI270 Example/I][10.806]: Performing CRT...
[Bmi270/I][10.856]: CRT: After setup GYR_CRT_CONF=0x05, bit3(rdy_for_dl)=0
[Bmi270/I][10.876]: CRT: Iter[0] GYR_CRT_CONF=0x05, bit3=0
[Bmi270/I][11.056]: CRT: Iter[9] GYR_CRT_CONF=0x05, bit3=0
[Bmi270/I][11.256]: CRT: Iter[19] GYR_CRT_CONF=0x05, bit3=0
[Bmi270/W][14.856]: CRT: Ready for Download bit didn't toggle - may still continue
[Bmi270/I][14.916]: CRT: Completion detected at iteration 0
[Bmi270/I][14.916]: CRT completed successfully
[BMI270 Example/I][14.916]: CRT completed successfully
Types of changes
Checklist:
Software
.github/workflows/build.ymlfile to add my new test to the automated cloud build github action.